package com.whl_Holden.minio.utils;

import com.mongodb.client.result.DeleteResult;
import com.whl_Holden.minio.entity.FileLink;
import com.whl_Holden.minio.entity.UploadFile;
import io.minio.*;

import io.minio.errors.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;


@Component
public class MinioUtil {

    @Value("${minio.endpoint}")
    private String endpoint;

    @Value("${minio.access-key}")
    private String accessKey;

    @Value("${minio.secret-key}")
    private String secretKey;

    @Value("${minio.bucket}")
    private String bucket;

    @Resource
    private MinioClient minioClient;

    @Resource
    private MongoTemplate mongoTemplate;

    Logger logger = LoggerFactory.getLogger(MinioUtil.class);


    /**
     * @param md5        文件md5值
     * @param fileSuffix 后缀
     * @return 是否存在该文件
     */
    public Boolean checkExist(String md5, String fileSuffix) {
        boolean exits = true;
        try {
            minioClient.statObject(StatObjectArgs.builder().bucket(bucket).object("files/" + md5 + fileSuffix).build());
        } catch (Exception e) {
            exits = false;
        }
        return exits;
    }

    /**
     * 检查文件列表中的各个文件是否存在
     * @param md5List
     * @return
     */
    public List<String> checkFilesExits(List<UploadFile> md5List) {
        ArrayList<String> noExitList = new ArrayList<>();
        md5List.forEach(item -> {
            try {
                logger.info("校验是否存在的文件名是： files/" + item.getMd5() + item.getFileSuffix());
                minioClient.statObject(StatObjectArgs.builder().bucket(bucket).object("files/" + item.getMd5() + item.getFileSuffix()).build());
            } catch (Exception e) {
                logger.info("该文件需要上传，md5值是：" + item.getMd5());
                // 未存在的文件添加到队列中，前端仍需上传
                noExitList.add(item.getMd5());
            }
        });
        return noExitList;
    }

    /**
     * 上传文件分块
     *
     * @param md5         文件md5值
     * @param chunkIndex  分块索引
     * @param inputStream 分块数据流
     * @param fileSize    分块文件大小
     * @return 上传结果
     */
    public Boolean upload(String md5, Integer chunkIndex, InputStream inputStream, long fileSize) {
        logger.info("开始上传, 文件" + md5);
        logger.info("开始上传, 文件" + chunkIndex);

        boolean exits = true; // 存在该分块了
        // 判断分块是否存在
        try {
            minioClient.statObject(StatObjectArgs.builder().bucket(bucket).object("chunks/" + md5 + "/" + chunkIndex).build());
        } catch (Exception e) {
            logger.info("该分块不存在，需要上传");
            exits = false;
        }
        // 如果存在，直接返回true,否则进行上传分片
        if (exits) {
            logger.info("该分块存在，无需上传...");
            return exits;
        }
        logger.info("第" + chunkIndex + "分块开始上传...");
        // Upload known sized input stream.
        try {
            minioClient.putObject(
                    PutObjectArgs.builder().bucket(bucket).object("chunks/" + md5 + "/" + chunkIndex).stream(
                            inputStream, fileSize, -1).build());
        } catch (Exception e) {
            logger.error("上传分块错误", e);
        }
        try {
            inputStream.close();
        } catch (IOException e) {
            logger.error("关闭流失败", e);
            throw new RuntimeException(e);
        }
        logger.info("上传分片结束");
        return true;
    }


    /**
     * 通过MD5获取某个文件的分块列表
     *
     * @param md5
     * @return 分块列表
     */
    public List<Item> getChunkList(String md5) {
        Iterable<Result<Item>> results = minioClient.listObjects(
                ListObjectsArgs.builder()
                        .bucket(bucket)
                        .prefix("chunks/" + md5 + "/")
                        .recursive(false) // 只有一层，不必递归
                        .build());
        ArrayList<Item> items = new ArrayList<>();
        results.forEach(result -> {
            try {
                items.add(result.get());
            } catch (Exception e) {
                logger.error("获取分块列表报错", e);
            }
        });
        // 需要进行排序再进行Compose
        items.sort((o1, o2) -> {
            String o1Name = o1.objectName(); // 返回的是在minio bucket中的路径
            String o2Name = o2.objectName();
            int o1Index = Integer.parseInt(o1Name.substring(o1Name.lastIndexOf("/") + 1));
            int o2Index = Integer.parseInt(o2Name.substring(o2Name.lastIndexOf("/") + 1));
            return o1Index - o2Index; // 谁小谁在前
        });
        return items;
    }


    /**
     * 获取缺失的分块列表
     *
     * @param chunkList
     * @param chunkTotal
     * @return 缺失的分块列表
     */
    private List<Integer> getMissChunkIndexList(List<Item> chunkList, Integer chunkTotal) {
        /**
         * 需要找出缺失块，
         * 1、首先，minio中md存储的分块文件名以md5命名，其中临时分块是按照1~100等排序
         * 2、chunkTotal为前端分块数量，正常情况minio中得到的分块列表元素数量应该与其一致，
         * 3、若存在漏传或者断传情况，则添加到missChunkIndexList中
         */
        List<Integer> missChunkIndexList = new ArrayList<>(chunkTotal);
        if (!chunkList.isEmpty()) { // 存在部分上传的情况
            for (int i = 1; i < chunkTotal; i++) {
                for (int j = i; j < chunkList.size(); j++) {
                    String chunkName = chunkList.get(i - 1).objectName();
                    int index = Integer.parseInt(chunkName.substring(chunkName.lastIndexOf("/") + 1));
                    if (index != i) {
                        missChunkIndexList.add(i);
                        i++;
                        break;
                    }
                }
            }
        } else {
            // 使用IntStream生成从1到chunkTotal(包含)的整数流，并收集到missChunkIndexList中
            missChunkIndexList = IntStream.rangeClosed(1, chunkTotal)
                    .boxed()  // 将int转换为Integer
                    .collect(Collectors.toList());  //收集到列表中
        }
        return missChunkIndexList;
    }

    /**
     * 合并文件
     *
     * @param md5
     * @param chunkTotal
     * @param fileSuffix
     * @return
     */
    public Boolean merge(String md5, Integer chunkTotal, String fileSuffix) {
        logger.info("开始合并");
        // 获取所有分块
        List<Item> itemList = getChunkList(md5);

        // 获取缺失的分块
        List<Integer> missChunkIndexList = getMissChunkIndexList(itemList, chunkTotal);

        if (!missChunkIndexList.isEmpty()) { // 缺失上传分块
            logger.warn("缺失分块，缺失列表为：{}，需要重新上传", missChunkIndexList);
            return false;
        }

        // 合并文件
        List<ComposeSource> sourceObjectList = new ArrayList<ComposeSource>();
        for (Item item : itemList) {
            sourceObjectList.add(
                    ComposeSource.builder().bucket(bucket).object(item.objectName()).build());
        }

        // 合并文件
        try {
            minioClient.composeObject(
                    ComposeObjectArgs.builder()
                            .bucket(bucket)
                            .object("files/" + md5 + fileSuffix)
                            .sources(sourceObjectList)
                            .build());

        } catch (Exception e) {
            logger.warn("合并过程中报错：", e);
            logger.warn("合并失败...");
            return false;
        }
        logger.info("合并成功了...");
//        http://192.168.123.101:9000/sph/files/71149ab18f8279400e4ba5269ae295f5.jpg
        String url = endpoint + "/" + bucket + "/" + "files/" + md5 + fileSuffix;
        logger.info("插入的的url是：" + url);
        Boolean insertResult = this.insertUrl(url);
        Boolean deleteResult = this.deleteChunks(md5);
        logger.info(insertResult.toString());
        logger.info(deleteResult.toString());
        if (insertResult && deleteResult) {
            logger.info("插入成功了...");
            return true;
        } else {
            logger.error("插入失败...");
            return false;
        }


    }


    /**
     * mongDB中插入Url记录，并返回插入结果
     *
     * @param url
     * @return
     */
    public Boolean insertUrl(String url) {
        FileLink fileLink = new FileLink();
        fileLink.setUrl(url);
        mongoTemplate.insert(fileLink);
        List<FileLink> fileLinks = mongoTemplate.find(Query.query(Criteria.where("url").is(url)), FileLink.class);
        return !fileLinks.isEmpty();
    }

    /**
     * 删除mongoDB中记录，并返回删除结果
     *
     * @param url
     * @return
     */
    public Boolean deleteUrl(String url) {
        Query query = new Query();
        query.addCriteria(Criteria.where("url").is(url));
        DeleteResult result = mongoTemplate.remove(query, FileLink.class);
        return result.getDeletedCount() > 0;
    }

    /**
     * 获取mongDB中的记录
     *
     * @return
     */
    public List<FileLink> getFileLinkList() {
        logger.info("获取已上传的文件列表...");
        return mongoTemplate.findAll(FileLink.class);
    }

    /**
     * 删除文件
     *
     * @param url
     * @return
     */
    public Boolean deleteFileLink(String url) {
        System.out.println("url是：" + url);
        String filePos = url.substring(url.lastIndexOf("files/"));  // 文件位置
        System.out.println("filePos是：" + filePos);
        String md5 = filePos.substring(filePos.lastIndexOf("/") + 1, filePos.lastIndexOf("."));
        System.out.println("md5是：" + md5);


        // 1、minio中查找文件并删除
        boolean isExit = true;
        try {
            minioClient.statObject(
                    StatObjectArgs.builder().bucket(bucket).object(filePos).build());
        } catch (Exception e) {
            isExit = false;
        }


        if (isExit) {
            try {
                minioClient.removeObject(
                        RemoveObjectArgs.builder().bucket(bucket).object(filePos).build());
            } catch (Exception e) {
                logger.warn("minio中删除该文件报错:" + e.getMessage());
                return false;
            }
        }
        logger.info("minio是否存在该文件？" + (isExit ? "存在" : "不存在"));
        // 2、mongoDB中查找文件中的记录
        Query query = new Query();
        query.addCriteria(Criteria.where("url").is(url));
        FileLink fileLink = mongoTemplate.findOne(query, FileLink.class);
        if (fileLink != null) {
            DeleteResult result = mongoTemplate.remove(fileLink);
            return result.getDeletedCount() > 0;
        } else {
            logger.warn("该记录不存在，已被删除");
            return false;
        }


    }


    /**
     * 删除分块
     *
     * @param md5
     * @return
     */
    public Boolean deleteChunks(String md5) {
        logger.info("开始删除分块...");
        // 1、查找分块并删除，如果没有分块
        List<Item> chunkList = getChunkList(md5);  // 获取chunks/{md5}/下所有文件的列表
        List<DeleteObject> delChunkList = new LinkedList<>(); // 创建为删除队列

        List<String> nameList = new LinkedList<>(); //  分块位置列表
        for (Item item : chunkList) {
            nameList.add(item.objectName());
        }
        logger.info("查到的chunklist" + nameList);
        for (String objectName : nameList) {
            logger.info("name是:" + objectName);
            delChunkList.add(new DeleteObject(objectName));
        }

        Iterable<Result<DeleteError>> results =
                minioClient.removeObjects(
                        RemoveObjectsArgs.builder().bucket(bucket).objects(delChunkList).build());

        if (results.iterator().hasNext()) {
            for (Result<DeleteError> result : results) {
                DeleteError error = null;
                try {
                    error = result.get();
                    logger.error("删除分块时的错误是：" + error);
                } catch (Exception e) {
                    logger.warn("删除分块抛出的异常是：" + e.getMessage());
                    throw new RuntimeException(e);
                }
                System.out.println(
                        "Error in deleting object " + error.objectName() + "; " + error.message());
            }
            logger.info("结束删除分块1...");
            return false;
        } else {
            logger.info("结束删除分块2...");
            return true;
        }

    }

}
